import copy
import pygame
from pygame.locals import *
import sys
import Projectile
import G
import Util
import ConfigParser
import re
import math
import Configurable
import Vector
import Config
import Animation
import Item
from Timekeeper import Timekeeper
from collections import deque

proj_re = re.compile(r'^projectile_(.*)')

aimdict = Config.aimdict

class Weapon(Item.Item, Timekeeper):
   runtime_vars = ('fire_cooldown',
                   'sync_fire_cooldown',
                   'alt_fire_cooldown',
                   'shotindex',
                   'angleindex',
                   'shotoffsetslen',
                   'angleslen',
                   'weaponpoint',
                   'offset',
                   'def_muzzle_velocities',
                   'fire_delay',
                   'hitscan',
                   'energy_cost_real',
                   'ammo',
                   'activated',
                   'fire_times')
   
   depths = {'angles': 2,
             'hitscan_color': 2,
             'shotoffsets': 3}

   __slots__ = ('proj',
                'fire_rate',
                'projectile_speed',
                'angles',
                'weight',
                'shotoffsets',
                'alternate',
                'level',
                'maxlevels',
                'aim',
                'hitscan_color',
                'energy_cost',
                'max_ammo',
                'ammo_per_shot',
                'side') + runtime_vars
   
   def __init__(self, fire_rate, projectile_speed, weight, proj,
                angles=[0.0], alternate=True, shotoffsets=[[0,0]], level=0, 
                maxlevels=1, aim='aim_normal', hitscan_color=(255,255,255),
                energy_cost=0.0, max_ammo=-1, ammo_per_shot=None, side=None, **args):
      Item.Item.__init__(self, **args)
      self.maxlevels = maxlevels
      self.hitscan = [False for i in range(0, self.maxlevels)]
      self.activated = True
      self.fire_times = deque()
      self.weaponpoint = None
      self.side = side
      
      #Angles and shotoffsets need to be nested lists--check if they are
      #too shallow
      if Util.maxdepth(angles) == self.__class__.depths['angles'] - 2:
         angles = Util.shallownest(angles, 1)
         
      if Util.maxdepth(shotoffsets) == self.__class__.depths['shotoffsets'] - 2:
         shotoffsets = Util.shallownest(shotoffsets, 2)
      elif Util.maxdepth(shotoffsets) == self.__class__.depths['shotoffsets'] - 3:
         shotoffsets = [[0,0]]
      
      ls = locals()
      ls.update(args)
      for var in Item.Item.config_vars():
         if not ls.has_key(var):
            ls[var] = getattr(self, var)
      #Set variables--some are simple values, others need to be repeated per level
      for var in self.__class__.config_vars():
         if var in ('level', 'weight', 'alternate'):
            setattr(self, var, ls[var])
         elif var not in ['proj', 'maxlevels', 'name', 'side']:
            if ls.has_key(var):
               value = ls[var]
               if var in self.__class__.depths.keys():
                  depth = self.__class__.depths[var]
               else:
                  depth = 1
               if Util.maxdepth(value) < depth:
                  setattr(self, var, [copy.deepcopy(value) for i in range(0, self.maxlevels)])
               else:
                  setattr(self, var, copy.deepcopy(value))
      
      self.icon = [None] * self.maxlevels
      if self.iconpath is not None:
         for l, ic in enumerate(self.icon):
            if self.iconpath[l] is not None:
               self.icon[l] = Util.load_image(self.iconpath[l])[0]
      
      for i,a in enumerate(self.aim):
         self.aim[i] = aimdict[a]
      
      for i,s in enumerate(self.projectile_speed):
         if isinstance(s, float):
            self.projectile_speed[i] = int(s)
         elif isinstance(s, str):
            if s == 'hitscan':
               self.projectile_speed[i] = 100
               self.hitscan[i] = True
            else:
               error('Unrecognized projectile speed for %s' % self.name)
      
      #Set up projectile vars list--one entry for each level
      self.proj = [[] for i in range(0, self.maxlevels)]
      proj_vars = list()
      for level in range(0, self.maxlevels):
         proj_dict = dict()
         for key, v in proj.items():
            value = Util.str_convert(v)
            if not Util.is_sequence(value):
               proj_dict[key] = value
            else:
               proj_dict[key] = value[level]
         if 'top_speed' not in proj_dict:
            proj_dict['top_speed'] = self.projectile_speed[level]
         proj_vars.append(proj_dict)
      
      #Create the projectiles
      for i, entry in enumerate(proj_vars):
         self.proj[i] = Projectile.Projectile(**entry)
         self.proj[i].elongate(self.proj[i].top_speed)
      
      #Initialize default muzzle velocities
      self.def_muzzle_velocities = [[] for level in self.angles]
      for i, level in enumerate(self.angles):
         self.def_muzzle_velocities[i] = [[] for ang in level]
         for j, ang in enumerate(level):
            self.angles[i][j] = math.pi / 2 - ang * math.pi / 180
      self.calculate_def_muzzle_velocities()
      
      self.shotindex = 0
      self.angleindex = 0
      self.fire_delay = 0.0
      self.set_level(self.level)
      for i, x in enumerate(self.ammo_per_shot):
         if x is None:
            self.ammo_per_shot[i] = 1
      self.ammo = self.max_ammo[self.level]
      if alternate:
         self.start_alternate()
      else:
         self.stop_alternate()
   
   def alternate_fire(self):
      if self.alternate:
         self.stop_alternate()
      else:
         self.start_alternate()
   
   def ammo_drain(self, amount=None):
      if amount is None:
         amount = self.ammo_per_shot[self.level]
      if self.ammo > 0:
         self.ammo -= amount
         if self.ammo <= 0:
            self.ammo = 0
            self.energy_cost_real = self.get_energy_cost()
   
   def ammo_refill(self, amount=0, proportion=None):
      if self.ammo == 0:
         recalculate = True
      else: 
         recalculate = False
      if proportion is not None:
         amount = int(math.ceil(proportion * self.max_ammo[self.level]))
      if amount > 0:
         self.ammo = Util.constrain(self.ammo + amount, 0, self.max_ammo[self.level])
         if recalculate:
            self.energy_cost_real = self.get_energy_cost()
   
   def augmented_description(self):
      proj = self.proj[self.level]
      if self.hitscan[self.level]:
         speed = "Instant"
      else:
         speed = str(proj.top_speed).ljust(7)
      lines = list()
      if proj.acceleration == 0:
         speedname = "Projectile Speed"
      else:
         speedname = "       Top Speed"
      lines.append("%s\nCost: %s Level: %s Weight: %d\nDamage: %s Fire Rate: %s Damage Type: %s\n%s: %s Shot Energy Cost: %d" % \
         (self.name, str(self.value[self.level]).ljust(8), str(self.level).ljust(3),
          self.weight, str(proj.damage).ljust(5), ("%s / sec" % str(self.fire_rate[self.level])).ljust(11),
          proj.type.title(), speedname, speed, self.energy_cost[self.level]))
      if self.max_ammo[self.level] > -1:
         lines.append("Ammo: %s Ammo Per Shot: %d" % (str(self.max_ammo[self.level]).ljust(5), self.ammo_per_shot[self.level]))
      if proj.home_type != 0:
         lines.append("Home Turn Speed: %d deg/sec" % (int(proj.home_turn_speed * 180 / math.pi)))
      if proj.explode_damage != 0:
         lines.append("Explode Damage: %s Explode Radius: %d" % (str(proj.explode_damage).ljust(5), proj.explode_radius))
      if proj.acceleration != 0:
         lines.append("Initial Speed: %s Acceleration %d / sec^2" % (str(self.projectile_speed[self.level]).ljust(5), proj.acceleration))
      lines.append('\n%s' % self.description[self.level])
      return '\n'.join(lines)

   def calculate_def_muzzle_velocities(self):
      for i, level in enumerate(self.angles):
         for j, ang in enumerate(level):
            self.def_muzzle_velocities[i][j] = (int(self.projectile_speed[i] * math.cos(self.angles[i][j])),
                               int(self.projectile_speed[i] * math.sin(self.angles[i][j])))

   def copy(self):
      return Configurable.instantiate(G.items[self.name], level=self.level)
   
   def fire(self, angle=None, group=G.projectiles, screen=None):
      #Define some useful variables based on allegiance
      if self.holder.friendly:
         ymult = -1
         friendly = True
      else:
         ymult = 1
         friendly = False
      pos = (self.holder.rect.left + self.offset[0], self.holder.rect.top + self.offset[1])
      #Calculate the muzzle velocities--first from a supplied angle (if any),
      #then if the weapon has auto-aim enabled, and finally the defaults
      if angle is not None:
         muzzle_velocities = [(int(self.projectile_speed[self.level] * math.cos(angle + a - math.pi/2)),
                            int(self.projectile_speed[self.level] * math.sin(angle + a - math.pi/2))) for a in self.angles[self.level]]
      elif self.aim[self.level] != 0 and self.holder.target is not None:
         target = self.holder.target
         if self.aim[self.level] == 1:
            dest = target.rect.center
         elif self.aim[self.level] == 2:
            dest = Vector.lead_target(pos,
                                      self.projectile_speed[self.level],
                                      target.rect.center,
                                      target.velocity)
            if dest is None:
               dest = target.rect.center
         vector_to = Vector.v_sub(dest, pos)
         angle = Vector.angle(vector_to)
         muzzle_velocities = [(int(self.projectile_speed[self.level] * math.cos(angle + a - math.pi/2)),
                            ymult * int(self.projectile_speed[self.level] * math.sin(angle + a - math.pi/2))) for a in self.angles[self.level]]
      else:
         muzzle_velocities = self.def_muzzle_velocities[self.level]
      
      if self.alternate:
         mv_real = (muzzle_velocities[self.angleindex][0],
                    muzzle_velocities[self.angleindex][1] * ymult)
         offset = self.shotoffsets[self.level][self.shotindex]
         pos_real = (pos[0] + offset[0], 
                     pos[1] + offset[1] * ymult * -1)
         if self.hitscan[self.level]:
            intercept = Vector.border_intersect(pos_real, mv_real, Config.width, Config.height)
            Animation.hitscan_trail(pos_real, intercept, 
                                    self.proj[self.level].damage, 
                                    self.proj[self.level].type,
                                    self.hitscan_color[self.level],
                                    self.proj[self.level].hits, self.holder,
                                    screen=screen)
         else:
            Projectile.fire(self.proj[self.level], 
                            self.holder,
                            pos_real[0],
                            pos_real[1],
                            mv_real[0],
                            mv_real[1],
                            friendly,
                            group)
         self.shotindex = (self.shotindex + 1) % self.shotoffsetslen
         self.angleindex = (self.angleindex + 1) % self.angleslen
         self.ammo_drain()
      else:
         for mv in muzzle_velocities:
            mv_real = (mv[0], mv[1] * ymult)
            for offset in self.shotoffsets[self.level]:
               pos_real = (pos[0] + offset[0], 
                           pos[1] + offset[1] * ymult * -1)
               if self.hitscan[self.level]:
                  intercept = Vector.border_intersect(pos_real, mv_real, Config.width, Config.height)
                  Animation.hitscan_trail(pos_real, intercept, 
                                          self.proj[self.level].damage, 
                                          self.proj[self.level].type, 
                                          self.hitscan_color[self.level], 
                                          self.proj[self.level].hits, self.holder,
                                          screen=screen)
               else:
                  Projectile.fire(self.proj[self.level], 
                                  self.holder,
                                  pos_real[0],
                                  pos_real[1],
                                  mv_real[0],
                                  mv_real[1],
                                  friendly,
                                  group)
               self.ammo_drain()
      self.fire_delay += self.fire_cooldown
      
      self.holder.drain(self.energy_cost_real)
      #if self.holder.friendly:
      #   self.fire_times.append(Timekeeper.last_updated)
   
   @classmethod
   def from_file(cls, items):
      weapon_vars = dict()
      proj_list = list()
      other_vars = dict()
      for key,val in items:
         if key in cls.config_vars():
            weapon_vars[key] = Util.str_convert(val)
         elif key.startswith('projectile_'):
             key_real = proj_re.match(key).group(1)
             if key_real in Projectile.Projectile.config_vars():
               proj_list.append((key_real, Util.str_convert(val)))
         else:
            other_vars[key] = Util.str_convert(val)
      if 'rps' not in other_vars.keys() and 'rpm' in other_vars.keys():
         rpm = other_vars['rpm']
         if Util.is_sequence(rpm):
            weapon_vars['fire_rate'] = [o / 60.0 for o in rpm]
         else:
            weapon_vars['fire_rate'] = rpm / 60.0
      else:
         rps = other_vars['rps']
         if Util.is_sequence(rps):
            weapon_vars['fire_rate'] = [float(o) for o in rps]
         else:
            weapon_vars['fire_rate'] = float(rps)
      weapon_vars['proj'] = dict(proj_list)
      cls.check_missing(weapon_vars)
      weapon_vars['type'] = cls
      return weapon_vars
         
   def get_description(self):
      return self.lattr('description')
   
   def get_energy_cost(self):
      if self.activated and self.ammo <= 0:
         if self.alternate:
            return self.energy_cost[self.level]
         else:
            return self.energy_cost[self.level] * len(self.def_muzzle_velocities[self.level]) * len(self.shotoffsets[self.level])
      else:
         return 0
   
   def get_fire_rate(self):
      fr = self.lattr('fire_rate')
      if self.alternate:
         return fr
      else:
         return fr / (len(self.angles[self.level]) * len(self.shotoffsets[self.level]))
   
   def get_icon(self):
      return self.lattr('icon')
   
   def get_name(self):
      return "Level " + str(self.level + 1) + " " + self.name
   
   def get_value(self):
      return self.lattr('value')
   
   def lattr(self, var):
      attr = getattr(self, var)
      if Util.is_sequence(attr):
         return attr[self.level]
      else:
         return attr
   
   def level_up(self):
      if self.level < self.maxlevels - 1:
         self.set_level(self.level + 1)
   
   def set_level(self, level):
      self.level = level
      self.shotoffsetslen = len(self.shotoffsets[self.level])
      self.angleslen = len(self.angles[self.level])
      self.sync_fire_cooldown = 1.0 * self.shotoffsetslen * self.angleslen / self.fire_rate[self.level]
      self.alt_fire_cooldown = 1.0 / self.fire_rate[self.level]
      if self.alternate:
         self.fire_cooldown = self.alt_fire_cooldown
         self.shotindex = 0
      else:
         self.fire_cooldown = self.sync_fire_cooldown
         self.shotindex = 0
   
   def start_alternate(self):
      self.alternate = True
      self.energy_cost_real = self.get_energy_cost()
      self.fire_cooldown = self.alt_fire_cooldown
      self.shotindex = 0
   
   def stop_alternate(self):
      self.alternate = False
      self.energy_cost_real = self.get_energy_cost()
      self.fire_cooldown = self.sync_fire_cooldown
      self.shotindex = 0
   
   def toggle(self):
      self.activated = not self.activated
      self.energy_cost_real = self.get_energy_cost()
      self.icon[self.level].set_alpha(255 - (1 - self.activated) * 127)
   
   def true_fire_rate(self):
      return len(self.fire_times)
   
   def update(self):
      if self.fire_delay > 0.0:
         self.fire_delay -= self.frame()
      #if self.holder.friendly:
      #   lu = Timekeeper.last_updated
      #   while len(self.fire_times) > 0 and self.fire_times[0] < lu - 1.0:
      #      self.fire_times.popleft()
